Decorators 101


In [1]:
def deco(func):
    def inner():
        print('running inner()')
    return inner

In [2]:
@deco
def target():
    print('running target()')

In [3]:
target()


running inner()

When Python Executes Decorators


In [12]:
registry= []

def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func

@register
def f1():
    print('running f1()')
    
@register
def f2():
    print('running f2()')
    
def f3():
    print('running f3()')


running register(<function f1 at 0x000000F990EB1730>)
running register(<function f2 at 0x000000F990EB1F28>)

In [13]:
def main():
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

In [14]:
main()


running main()
registry -> [<function f1 at 0x000000F990EB1730>, <function f2 at 0x000000F990EB1F28>]
running f1()
running f2()
running f3()

Decorator-Enhanced Strategy Pattern


In [15]:
promos = []

def promotion(promo_func):
    promos.append(promo_func)
    return promo_func

@promotion
def fidelity(order):
    """5% discount for customers with 1000 or more fidelity points"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0

@promotion
def bulk_item(order):
    """10% discount for each LineItem with 20 or more units"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount

@promotion
def large_order(order):
    """7% discount for oders with 10 or more distinct items"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0

def best_promo(order):
    """Select best discount available"""
    return max(promo(order) for promo in promos)

Variable Scope Rules


In [16]:
def f1(a):
    print(a)
    print(b)

In [17]:
f1(3)


3
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-17-a636accabfae> in <module>()
----> 1 f1(3)

<ipython-input-16-06ba9b782704> in f1(a)
      1 def f1(a):
      2     print(a)
----> 3     print(b)

NameError: name 'b' is not defined

In [18]:
b=6
f1(3)


3
6

In [19]:
def f2(a):
    print(a)
    print(b)
    b=9

In [20]:
f2(3)


3
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-20-495132a335c0> in <module>()
----> 1 f2(3)

<ipython-input-19-503d6e658025> in f2(a)
      1 def f2(a):
      2     print(a)
----> 3     print(b)
      4     b=9

UnboundLocalError: local variable 'b' referenced before assignment

In [21]:
def f3(a):
    global b
    print(a)
    print(b)
    b=9

In [22]:
f3(3)


3
6

Closures


In [24]:
class Averager():
    
    def __init__(self):
        self.series = []
        
    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

In [25]:
avg = Averager()

In [26]:
avg(10)


Out[26]:
10.0

In [27]:
avg(11)


Out[27]:
10.5

In [28]:
avg(12)


Out[28]:
11.0

In [30]:
def make_averager():
    series = []
    
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    
    return averager

In [31]:
avg = make_averager()

In [32]:
avg(10)


Out[32]:
10.0

In [33]:
avg(11)


Out[33]:
10.5

In [34]:
avg(12)


Out[34]:
11.0

In [35]:
avg.__code__.co_varnames


Out[35]:
('new_value', 'total')

In [36]:
avg.__code__.co_freevars


Out[36]:
('series',)

In [37]:
avg.__closure__


Out[37]:
(<cell at 0x000000F990E126D8: list object at 0x000000F990ECBF88>,)

In [38]:
avg.__closure__[0].cell_contents


Out[38]:
[10, 11, 12]

The nonlocal Declaration


In [41]:
def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    
    return averager

In [42]:
avg = make_averager()
avg(10)


Out[42]:
10.0

Implementing a Simple Decorator


In [46]:
import time

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

In [47]:
@clock
def snooze(seconds):
    time.sleep(seconds)
    
@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

In [49]:
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))


**************************************** Calling snooze(.123)
[0.12233035s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000197s] factorial(1) -> 1
[0.00004421s] factorial(2) -> 2
[0.00006987s] factorial(3) -> 6
[0.00009237s] factorial(4) -> 24
[0.00011606s] factorial(5) -> 120
[0.00013935s] factorial(6) -> 720
6! = 720

In [51]:
factorial.__name__


Out[51]:
'clocked'

In [52]:
import functools

In [53]:
def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

In [56]:
@clock
def snooze(seconds):
    time.sleep(seconds)
    
@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

In [57]:
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))


**************************************** Calling snooze(.123)
[0.12317157s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000000s] factorial(1) -> 1
[0.00000000s] factorial(2) -> 2
[0.00000000s] factorial(3) -> 6
[0.00000000s] factorial(4) -> 24
[0.00000000s] factorial(5) -> 120
[0.00000000s] factorial(6) -> 720
6! = 720

In [58]:
factorial.__name__


Out[58]:
'factorial'

Decorators in the Standard Library

Memoization with functools.lru_cache


In [7]:
from clockdeco import clock

In [4]:
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

In [5]:
print(fibonacci(6))


[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(4) -> 3
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00100040s] fibonacci(4) -> 3
[0.00100040s] fibonacci(5) -> 5
[0.00100040s] fibonacci(6) -> 8
8

In [8]:
import functools

In [9]:
@functools.lru_cache()
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

In [10]:
print(fibonacci(6))


[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(4) -> 3
[0.00000000s] fibonacci(5) -> 5
[0.00100279s] fibonacci(6) -> 8
8

Generic Functions with Single Dispatch


In [14]:
import html

def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

In [19]:
htmlize({1, 2, 3})


Out[19]:
'<pre>{1, 2, 3}</pre>'

In [20]:
htmlize(abs)


Out[20]:
'<pre>&lt;built-in function abs&gt;</pre>'

In [22]:
htmlize('Heimlich & co.\n- a game')


Out[22]:
'<pre>&#x27;Heimlich &amp; co.\\n- a game&#x27;</pre>'

In [24]:
htmlize(42)


Out[24]:
'<pre>42</pre>'

In [25]:
print(htmlize(['alpha', 66, {3, 2, 1}]))


<pre>[&#x27;alpha&#x27;, 66, {1, 2, 3}]</pre>

In [27]:
from functools import singledispatch
from collections import abc
import numbers
import html

In [28]:
@singledispatch
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

@htmlize.register(str)
def _(text):
    content = html.escape(text).replace('\n', '<br>\n')
    return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral)
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)

@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'

In [29]:
htmlize({1, 2, 3})


Out[29]:
'<pre>{1, 2, 3}</pre>'

In [30]:
htmlize(abs)


Out[30]:
'<pre>&lt;built-in function abs&gt;</pre>'

In [31]:
htmlize('Heimlich & co.\n- a game')


Out[31]:
'<p>Heimlich &amp; co.<br>\n- a game</p>'

In [32]:
htmlize(42)


Out[32]:
'<pre>42 (0x2a)</pre>'

In [33]:
print(htmlize(['alpha', 66, {3, 2, 1}]))


<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>

Parameterized Decorators


In [37]:
registry = []

def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func
    
@register
def f1():
    print('running f1()')


running register(<function f1 at 0x0000009A059396A8>)

In [38]:
print('running main()')
print('registry ->', registry)
f1()


running main()
registry -> [<function f1 at 0x0000009A059396A8>]
running f1()

A parameterized Registration Decorator


In [39]:
registry = set()

def register(active=True):
    def decorate(func):
        print('running register(active=%s)->decorate(%s)' % (active, func))
        if active:
            registry.add(func)
        else:
            registry.discard(func)
            
        return func
    return decorate

@register(active=False)
def f1():
    print('running f1()')
    
@register()
def f2():
    print('running f2()')
    
def f3():
    print('running f3()')


running register(active=False)->decorate(<function f1 at 0x0000009A058F39D8>)
running register(active=True)->decorate(<function f2 at 0x0000009A058F3BF8>)

In [40]:
f1()


running f1()

In [43]:
repr(registry)


Out[43]:
'{<function f2 at 0x0000009A058F3BF8>}'

In [44]:
from registration_param import *


running register(active=False)->decorate(<function f1 at 0x0000009A058F3EA0>)
running register(active=True)->decorate(<function f2 at 0x0000009A058F38C8>)

In [45]:
registry


Out[45]:
{<function registration_param.f2>}

In [46]:
register()(f3)


running register(active=True)->decorate(<function f3 at 0x0000009A058F3C80>)
Out[46]:
<function registration_param.f3>

In [47]:
registry


Out[47]:
{<function registration_param.f3>, <function registration_param.f2>}

In [48]:
register(active=False)(f2)


running register(active=False)->decorate(<function f2 at 0x0000009A058F38C8>)
Out[48]:
<function registration_param.f2>

In [49]:
registry


Out[49]:
{<function registration_param.f3>}

The parameterized Clock Decorator


In [52]:
import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

def clock(fmt=DEFAULT_FMT):
    def decorate(func):
        def clocked(*_args):
            t0 = time.time()
            _result = func(*_args)
            elapsed = time.time() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)
            result = repr(_result)
            print(fmt.format(**locals()))
            return result
        return clocked
    return decorate

In [54]:
@clock()
def snooze(seconds):
    time.sleep(seconds)
    
for i in range(3):
    snooze(.123)


[0.12301230s] snooze(0.123) -> None
[0.12375641s] snooze(0.123) -> None
[0.12382793s] snooze(0.123) -> None

In [60]:
import time
from clockdeco_param import clock

In [61]:
@clock('{name}: {elapsed}s')
def snooze(seconds):
    time.sleep(seconds)
    
for i in range(3):
    snooze(.123)


snooze: 0.12319779396057129s
snooze: 0.12392783164978027s
snooze: 0.12313723564147949s

In [62]:
@clock('{name}({args}) dt={elapsed:0.3f}s')
def snooze(seconds):
    time.sleep(seconds)
    
for i in range(3):
    snooze(.123)


snooze(0.123) dt=0.123s
snooze(0.123) dt=0.123s
snooze(0.123) dt=0.123s

In [ ]: